package ofc4j;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Collection;

import ofc4j.model.Chart;
import ofc4j.model.Text;
import ofc4j.model.axis.Axis;
import ofc4j.model.axis.Label;
import ofc4j.model.axis.XAxis;
import ofc4j.model.axis.XAxisLabels;
import ofc4j.model.axis.YAxis;
import ofc4j.model.elements.AreaHollowChart;
import ofc4j.model.elements.BarChart;
import ofc4j.model.elements.Element;
import ofc4j.model.elements.HorizontalBarChart;
import ofc4j.model.elements.LineChart;
import ofc4j.model.elements.PieChart;
import ofc4j.model.elements.ScatterChart;
import ofc4j.model.metadata.Alias;
import ofc4j.util.HorizontalBarChartBarConverter;
import ofc4j.util.PieChartSliceConverter;
import ofc4j.util.RotationConverter;
import ofc4j.util.ScatterChartPointConverter;

import org.json.JSONException;
import org.json.JSONObject;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver;

/**
 * This is the class responsible for converting a Chart object into the JSON
 * string which feeds the charting widget.  There is no need to make explicit
 * use of this class, but if necessary, there are several ways to do so:
 * 1.) The "instance" field contains a static instance of an OFC object.
 * 2.) The Chart object overrides toString() and uses this instance to render itself.
 * 3.) For tricky threading situations, you may prefer to create and manage instances of
 *     OFC yourself.  
 *     
 * Theoretically, XStream (the JSON conversion library used here) is thread-safe, but
 * it does not hurt to have the option to synchronize or to have thread local instances,
 * whatever may be necessary.
 */
public class OFC {
    private static final Class<?>[] models = new Class<?>[] {
        Chart.class, Axis.class, Text.class, XAxis.class, YAxis.class, 
        XAxisLabels.class, Label.class, Element.class, Axis.class, 
        BarChart.class, PieChart.class, HorizontalBarChart.class, 
        LineChart.class, ScatterChart.class, AreaHollowChart.class
    };
    
    public static final OFC instance = new OFC();
    
    private final XStream converter = new XStream(new JsonHierarchicalStreamDriver());
    
    public OFC() {
        //Configure XStream converter
        converter.registerConverter(new HorizontalBarChartBarConverter());
        converter.registerConverter(new PieChartSliceConverter());
        converter.registerConverter(new RotationConverter());
        converter.registerConverter(new ScatterChartPointConverter());
        
        //Search for metadata in model classes
        for (Class<?> c : models) {
            if (c.isAnnotationPresent(Alias.class)) {
                converter.alias(c.getAnnotation(Alias.class).value(), c);
            }
            for (Field f : c.getDeclaredFields()) {
                if (f.isAnnotationPresent(Alias.class)) {
                    converter.aliasField(f.getAnnotation(Alias.class).value(), c, f.getName());
                }
            }
        }
    }
    
    /**
     * Use this method in your applications to send data back to the chart widget.
     * @param c - the chart to render
     * @return the JSONified chart data
     */
    public String render(Chart c) {
        String json = converter.toXML(c);
        try {
            return new JSONObject(json).getString(Chart.class.getName());
        } catch (JSONException je) {
            System.err.println(json);
            je.printStackTrace();
            return null;
        }
    }
    
    /**
     * Use this method for debugging purposes.
     * @param c - the chart to render
     * @param indentationLevel - number of spaces to use for indentation
     * @return pretty-printed JSONified chart data
     */
    public String prettyPrint(Chart c, int indentationLevel) {
        String json = converter.toXML(c);
        try {
            return new JSONObject(json).getJSONObject(Chart.class.getName()).toString(indentationLevel);
        } catch (JSONException je) {
            System.err.println(json);
            je.printStackTrace();
            return null;
        }
    }
    
    /**
     * Convenience method for converting Collections to Arrays.  You can use this where the API
     * has limited support for collections:
     * getLabels().addLabels(OFC.toArray(stringList, String.class));
     * @param collection The collection to use
     * @param type The supertype for the collection.  This will commonly be Integer, Number, etc.
     * @return the array of the collection
     */
    @SuppressWarnings("unchecked")
    public static <T> T[] toArray(Collection<T> collection, Class<? extends T> type) {
        return collection.toArray((T[]) Array.newInstance(type, collection.size()));
    }
}
